errno 사용 규칙 — 초기화·저장·검사·설정 패턴

NOTE

환경변수·시그널·에러 처리 실습에서 errno 때문에 생긴 버그를 모아, 언제 0으로 만들고, 언제 읽고, 언제 저장하고, 언제 설정하는지를 한곳에 정리한 규칙 노트다. 실습 코드는 Docker Desktop의 Linux 컨테이너(c_app_part1)에서 gcc로 확인했다.

TIP

📌 한 페이지 요약

errno는 “마지막으로 실패한 함수의 이유”를 담는 **스레드별 int**다. 믿을 수 있는 순간이 짧아서 아래 규칙으로만 쓴다.

flowchart TD
    A["함수 호출"] --> B{"반환값이 실패인가?"}
    B -- "아니오" --> C["errno는 읽지 않는다<br/>성공해도 값이 바뀔 수 있음"]
    B -- "예" --> D{"읽기 전에 printf·로그·free·close를 부르는가?"}
    D -- "아니오" --> E["errno를 바로 읽는다<br/>perror · strerror"]
    D -- "예" --> F["int saved = errno;<br/>먼저 저장 후 정리"]
    F --> G["필요하면 errno = saved<br/>호출한 쪽에 전달"]
    H["strtol 계열처럼<br/>반환값만으로 구분 불가"] -.-> I["호출 직전에 errno = 0"]
    I -.-> A
#규칙이유
1실패는 반환값으로 먼저 판단하고, 그 뒤에 errno를 읽는다errno는 성공한 호출도 바꿀 수 있어서, errno != 0만으로는 실패를 알 수 없다
2errno로만 실패를 알리는 함수는 호출 직전에 errno = 0자동으로 0이 되지 않아 이전 값이 남는다
3읽기 직후가 아니면 저장한다 (int saved = errno;)로그·free·fclose·close가 값을 덮을 수 있다
4시그널 핸들러는 errno를 저장하고 복원한다핸들러 안의 write 등이 main이 보던 값을 바꾼다
5우리가 만드는 함수는 실패할 때만 errno를 설정한다성공 시 errno = 0으로 지우면 호출한 쪽의 이전 값을 망가뜨린다
6스트림(printf 등)의 오류는 errno가 아니라 ferror호출이 여러 개면 errno가 어느 호출 것인지 알 수 없다

1. 초기화 — errno = 0이 필요한 경우 (규칙 2)

요약

  • 문제: strtol처럼 반환값만으로 실패를 구분 못 하는 함수에서 이전 ERANGE가 남아 오판 (13-06)
  • 원인: errno는 성공해도 지워지지 않고, 프로그램 시작 때만 0
  • 처방: 그런 함수는 호출 직전에 errno = 0. 반환값(-1/NULL)으로 구분되는 함수는 반환값만 먼저 본다

함수 종류별로 errno를 쓰는 법 (교육자료 13-6)


2. 검사 — 판단은 반환값, 이유는 errno (규칙 1, 6)

요약

  • 문제: printf 여러 번 뒤 errno != 0 검사, if (errno = ERANGE) 대입 오타, 이유를 안 읽는 로그
  • 원인: 성공한 호출도 errno를 바꾸고, =는 대입이라 항상 참
  • 처방: 반환값 확인 → 그 뒤 errno == 비교, 스트림은 ferror, 이유는 perror/strerror

3. 저장 — 읽기 전에 덮어쓰이는 경우 (규칙 3, 4)

정리 코드와 로그 사이에서

요약

  • 문제: 실패 후 로그·free·fclose·close가 errno를 덮어씀
  • 원인: 정리 함수들이 내부에서 errno를 바꿀 수 있음
  • 처방: 실패 직후 int saved = errno;, 필요하면 errno = saved로 복원 (LOG_ERRNO가 같은 방식)

시그널 핸들러 (규칙 4)

요약

  • 문제: 핸들러 안의 write 등이 main이 읽으려던 errno를 바꿈
  • 원인: 핸들러는 어느 지점에서든 끼어듦
  • 처방: 핸들러 시작에서 저장, 끝에서 복원. 가장 안전한 것은 flag만 세우기

4. 설정 — 우리가 만드는 함수가 errno로 알릴 때 (규칙 5)

요약

  • 문제: NULL/-1로만 알리는 우리 함수는 실패 이유를 전달할 곳이 없음
  • 원인: 반환값에 이유를 담을 수 없음
  • 처방: 실패할 때만 errno 설정 (EINVAL/ERANGE/…), 성공 시 errno = 0으로 지우지 않음. 에러 코드를 반환값으로 돌려주는 방식도 가능

의미를 일관되게 쓰기


5. 재시도해야 하는 errno (EINTR, EAGAIN)

요약

  • 문제: EINTR을 오류로 처리해 실패 반환
  • 원인: 시그널이 호출을 끊었을 뿐 진짜 오류가 아님
  • 처방: nanosleep처럼 남은 시간(rem)으로 이어서 호출. EAGAIN은 바로 재시도하지 말고 select/poll로 대기

6. 메시지 출력

요약

  • 문제: strerror 반환 버퍼 공유, 라이브러리 함수가 직접 출력
  • 원인: strerror는 스레드 안전하지 않을 수 있음
  • 처방: perror · strerror_r · LOG_ERRNO 중 선택, 라이브러리 함수는 출력하지 않고 코드로만 알림

✅ 정리 — errno 점검 체크리스트